﻿using System.Collections.Generic;
using System.Linq;

namespace DJLang
{
    public class DJStd
    {
        public static class Arithmetics
        {
            [DJFunc("+")]
            public static IDJValue OpAdd(DJEnv _, DJSeq args)
            {
                if (args.Count <= 0) return new DJErr("Too few arguments");
                var first = args.Pop();

                if (args.Count == 0)
                {
                    if (first is not IAdd) return new DJErr($"Type {first.GetType()} do not supports add");
                    var clonedFirst = first.Clone();
                    return new DJBuiltin((env, seq) =>
                    {
                        var value = seq.PopAs<IAdd>();
                        return value.Add(clonedFirst);
                    });
                }

                while (args.Count > 0)
                {
                    if (first is not IAdd) return new DJErr($"Type {first.GetType()} do not supports add");
                    first = ((IAdd)first).Add(args.Pop());
                }
                return first;
            }

            [DJFunc("-")]
            public static IDJValue OpSub(DJEnv _, DJSeq args)
            {
                if (args.Count <= 0) return new DJErr("Too few arguments");
                var first = args.Pop();
                if (args.Count <= 0)
                {
                    if (first is not INeg) return new DJErr($"Trying to take negative to {first}");
                    return ((INeg)first).Neg();
                }

                while (args.Count > 0)
                {
                    if (first is not ISub) return new DJErr($"Type {first.GetType()} do not supports add");
                    first = ((ISub)first).Sub(args.Pop());
                }
                return first;
            }

            [DJFunc("*")]
            public static IDJValue OpMul(DJEnv _, DJSeq args)
            {
                if (args.Count <= 0) return new DJErr("Too few arguments");
                var first = args.Pop();
                while (args.Count > 0)
                {
                    if (first is not IMul) return new DJErr($"Type {first.GetType()} do not supports add");
                    first = ((IMul)first).Mul(args.Pop());
                }
                return first;
            }

            [DJFunc("/")]
            public static IDJValue OpDiv(DJEnv _, DJSeq args)
            {
                if (args.Count <= 0) return new DJErr("Too few arguments");
                var first = args.Pop();
                while (args.Count > 0)
                {
                    if (first is not IDiv) return new DJErr($"Type {first.GetType()} do not supports add");
                    first = ((IDiv)first).Div(args.Pop());
                }
                return first;
            }

            [DJFunc("not")]
            public static IDJValue OpNot(DJEnv _, DJSeq args)
            {
                var arg = args.Pop();
                if (arg is DJBasic<bool> b) return new DJBasic<bool>(!b.val);
                return new DJErr($"Can't take NOT for {arg}");
            }

            [DJFunc("and")]
            public static IDJValue OpAnd(DJEnv _, DJSeq args)
            {
                while (args.Count > 0)
                {
                    var value = args.Pop();
                    if (value is DJBasic<bool> b)
                    {
                        if (b.val == false) return new DJBasic<bool>(false);
                    }
                    else
                    {
                        return new DJErr($"Can't take AND for {value}");
                    }
                }

                return new DJBasic<bool>(true);
            }

            [DJFunc("or")]
            public static IDJValue OpOr(DJEnv _, DJSeq args)
            {
                while (args.Count > 0)
                {
                    var value = args.Pop();
                    if (value is DJBasic<bool> b)
                    {
                        if (b.val == true) return new DJBasic<bool>(true);
                    }
                    else
                    {
                        return new DJErr($"Can't take AND for {value}");
                    }
                }

                return new DJBasic<bool>(false);
            }

            [DJFunc("=")]
            public static IDJValue OpEq(DJEnv _, DJSeq args)
            {
                if (args.Count <= 0) return new DJErr("Too few arguments");
                var first = args.Pop();
                while (args.Count > 0)
                {
                    if (first is not IEq) return new DJErr($"Type {first} do not supports equal");

                    if (((IEq)first).Eq(args.Pop()) is DJBasic<bool> b && b.val == false) return new DJBasic<bool>(false);
                }
                return new DJBasic<bool>(true);
            }
        }

        public static class LanguageBasics
        {
            /// <summary>
            /// bind symbol to variable
            /// </summary>
            /// <param name="env">DJLang environment</param>
            /// <param name="args">Function args</param>
            /// <returns>null</returns>
            [DJSyntax("var")]
            public static IDJValue Bind(DJEnv env, DJSeq args)
            {
                var first = args.Pop();
                var second = args.Pop();
                if (first is DJList symbols && second is DJList variables)
                {
                    if (symbols.Count != variables.Count) return new DJErr($"Trying to bind {variables.Count} variables to {symbols.Count} symbols");
                    while (symbols.Count > 0)
                    {
                        var sym = symbols.Pop();
                        if (sym is DJSymbol symbol)
                        {
                            env.Put(symbol.name, variables.Pop());
                        }
                        else
                        {
                            return new DJErr($"{sym} is not symbol");
                        }
                    }
                    return new DJNil();
                }

                if (first is DJSymbol fst)
                {
                    env.Put(fst.name, second);
                    return new DJNil();
                }

                return new DJErr("Bind expect two lists");
            }

            [DJFunc("list")]
            public static IDJValue List(DJEnv _, DJSeq args)
            {
                return new DJList(args.ToArray());
            }

            [DJFunc("dictionary")]
            public static IDJValue Dict(DJEnv _, DJSeq args)
            {
                Dictionary<string, IDJValue> ans = new();
                while (args.Count > 0)
                {
                    var kvpair = args.PopAs<DJList>();
                    var key = kvpair.PopAs<DJSymbol>().name;
                    var value = kvpair.Pop();
                    ans.Add(key, value);
                }
                return new DJRef<Dictionary<string, IDJValue>>(ans);
            }

            /// <summary>
            /// e.g. (if #t [then-block] [else-block])
            /// </summary>
            /// <param name="env"></param>
            /// <param name="args"></param>
            /// <returns></returns>
            [DJFunc("if")]
            public static IDJValue If(DJEnv env, DJSeq args)
            {
                if (args.Pop() is not DJBasic<bool> judge) return new DJErr("first parameter must be bool");
                return judge.val ? env.ForceEval(args.Pop()) : env.ForceEval(args.Pop(1));
            }

            [DJFunc("cond")]
            public static IDJValue Cond(DJEnv env, DJSeq args)
            {
                while (args.Count > 0)
                {
                    var branch = args.PopAs<DJList>();
                    var judge = branch.Pop();
                    if (judge is DJSymbol s && s.name == "else")
                    {
                        return env.ForceEval(branch.Pop());
                    }

                    if (judge.Evaluate(env) is DJBasic<bool> b && b.val)
                    {
                        return env.ForceEval(branch.Pop());
                    }
                }
                return new DJNil();
            }

            [DJFunc("let")]
            public static IDJValue Let(DJEnv env, DJSeq args)
            {
                DJEnv local = new()
                {
                    parent = env
                };
                var localVarList = args.PopAs<DJList>();
                while (localVarList.Count > 0)
                {
                    var defPair = localVarList.PopAs<DJList>();
                    var symbol = defPair.PopAs<DJSymbol>();
                    var value = defPair.Pop().Evaluate(env);
                    local.Put(symbol, value);
                }
                return local.ForceEval(args.Pop());
            }

            [DJFunc("car")]
            public static IDJValue Car(DJEnv env, DJSeq args)
            {
                var fst = args.Pop();
                if (fst is DJSeq s)
                    return s.Pop();
                return fst;
            }

            [DJFunc("cdr")]
            public static IDJValue Cdr(DJEnv env, DJSeq args)
            {
                var fst = args.Pop();
                if (fst is DJSeq s)
                {
                    s.Pop();
                    return s;
                }
                return new DJList();
            }

            [DJFunc("cadr")]
            public static IDJValue Cadr(DJEnv env, DJSeq args)
            {
                if (args.Pop() is DJSeq s)
                {
                    return s.Pop(1);
                }
                return new DJNil();
            }

            [DJFunc("map")]
            public static IDJValue Map(DJEnv env, DJSeq args)
            {
                var f = args.PopAs<IDJCallable>();
                var s = args.PopAs<DJSeq>();

                var len = s.Count;

                while (len > 0)
                {
                    var ff = f.Clone() as IDJCallable;
                    s.Push(ff.Call(env, new DJList(s.Pop())));
                    len--;
                }
                return s;
            }

            [DJFunc("append")]
            public static IDJValue Append(DJEnv env, DJSeq args)
            {
                var seq = args.Pop() as DJSeq;
                while (args.Count > 0)
                {
                    seq.Push(args.Pop());
                }
                return seq;
            }

            [DJFunc("lambda")]
            public static IDJValue Lambda(DJEnv _, DJSeq args)
            {
                var formals = args.Pop();
                if (formals is not DJList) return new DJErr("lambda expect formal parameter list as the first parameter");
                return new DJClosure(formals, args.Pop());
            }

            [DJFunc("fn")]
            public static IDJValue DefineFunction(DJEnv env, DJSeq args)
            {
                var nameAndFormals = args.PopAs<DJList>();
                var name = nameAndFormals.PopAs<DJSymbol>();
                var closure = new DJClosure(nameAndFormals, args.Pop());
                env.Put(name.name, closure);
                return closure;
            }

            [DJFunc("eval")]
            public static IDJValue Eval(DJEnv env, DJSeq args)
            {
                return env.ForceEval(args.Pop());
            }

            [DJFunc("call")]
            public static IDJValue Call(DJEnv env, DJSeq args)
            {
                var fst = args.Pop().Evaluate(env);
                return fst is IDJCallable f ? f.Call(env, args.PopAs<DJSeq>()) : new DJErr($"Unable to call {fst}");
            }

            [DJFunc("do")]
            public static IDJValue Do(DJEnv _, DJSeq args)
            {
                while (args.Count > 1) args.Pop();
                return args.Pop();
            }
        }

        public static class Debug
        {
            [DJFunc("ls-env")]
            public static IDJValue ListVariables(DJEnv env, DJSeq _)
            {
                return new DJList(env.variables.Select(kv => new DJString(kv.Key)));
            }
        }
    }
}
